In the context of this book, Oberon is the name of both an application framework and of a programming language. This is appropriate since it emphasizes that both are closely linked: the design of the framework relies on the expressiveness of the language (types, modules, object-orientation), while the language relies on services like garbage collection, which are provided by the framework.
Consequently, the name Oberon is used for both the framework and the language in this documentation. However, a minimal distinction is retained by denoting the framework as Oberon/F, and the language as Oberon/L. Sometimes the suffix "/L" is left out.
It is a major goal of Oberon/F to make modern non-modal user interfaces better accessible to programmers. While some of the current graphical user interfaces are very convenient to use, they are notoriously difficult to develop for. Oberon/F is a framework which not only hides much of this difficulty, but also most of the differences between the various popular graphical user interfaces. This makes it possible to write multi-platform applications, i.e. programs which can acquire the "native" look and feel of the platform they run on.
Oberon/L is a language in the tradition of Algol, Pascal, and Modula-2. Like Algol it has a well-defined and readable syntax, like Pascal it supports structured programming, and like Modula-2 it supports modular programming. Oberon retains the proven strengths of its predecessors, and additionally supports object-oriented and type-safe programming. Complete operating systems have been written in Oberon, yet the language is even simpler than its direct predecessor Modula-2. Since the syntax of Oberon is still close to the one of Pascal, experienced Pascal programmers take little time to become comfortable with Oberon.
Probably the most important - though rarely understood or exploited - advantage of object-oriented programming is the possibility to design extensible programs. Extensibility means that new features can be developed and integrated into an existing program, even if its source code is not available. Consequently, there is never a point in time when an extensible application can be considered "complete", and linking must be delayed until load-time (dynamic linking).
An operating system is a prime example of an extensible software system, since application programs extend the functionality of the base system. Another good example is a word processor. Oberon/F contains a basic text editor which can be extended by new subroutines, so-called commands. Such a command may be e.g. a spelling checker, which thus could be added later, possibly by a third party specialized in spelling checkers. A spelling checker which isn't used never occupies main memory, because there is no need to load it.
The Oberon/F text editor can be extended in another way as well. In Oberon/F, not only characters can flow in a text, but also arbitrary rectangular objects. New types of such objects can be added anytime, e.g. a chart object or an object which implements a printed circuit board editor. This object embedding capability is central to Oberon/F: there are no classic monolithic applications, instead there are software components which implement document parts. Component software is another name for extensible software. In Oberon/F, a software component consists of one or more Oberon/L modules.
To make Oberon/F software truly platform-independent, it is necessary to store the data that they generate in a platform-independent file format. For the important case of textual data, the upcoming Unicode character encoding standard is used. Binary data is handled in a machine-independent way as well. Wherever appropriate, graphical data is measured in device-independent units instead of pixels, in order to eliminate dependencies on the display resolution.
A large part of the development time of a program usually goes into debugging. Oberon/L eliminates much of this debugging time by allowing to program in a type-safe way. This means that the programmer can state static properties of a program, e.g. by specifying the size of an array, by declaring the minimal type of a variable, or by protecting the implementation of a module by hiding it. These properties are checked during compilation where possible, and at run-time where necessary. One particularly time-consuming type of programming error is not possible at all in Oberon/L: memory cannot inadvertantly be released when it is still being referenced. Instead, memory is automatically released when it isn't referenced anymore, by a built-in service called a garbage-collector. The importance of type-safe programming for programmer productivity and program reliability cannot be stressed enough.
Oberon/L is a successful combination of "static" languages like Pascal with "dynamic" languages like Smalltalk. It retains the static expressiveness of types and modules, which are most important tools to express the architecture of a software system. As an additional benefit, simple techniques can be used to build native-code compilers, i.e. no slow interpretation or wasteful caching techniques are needed. On the other hand, types can be extended, and code can be reused and loaded at run-time like in dynamic object-oriented languages.
Oberon/L conforms to the Oberon-2 standard as published by ETH. See the language report in "Oberon/L, Language, Reference".
The advantages of the programming language Oberon cannot be fully utilized without an appropriate run-time system: Oberon/F provides such a run-time system which supports commands, dynamic loading, as well as garbage collection and type safety. Beyond that, Oberon/F is also an integrated development environment, which provides a fast Oberon/L compiler, an extensible text editor, and an extensible forms editor. Last but not least, Oberon/F is a framework, i.e. a class library which allows the development of new interactive applications through reuse of existing interfaces and code.
1.2 From Applications to Application Frameworks
An application framework is a semi-finished product. It implements generic parts of an application, i.e. parts which are the same - or at least similar - for every similar kind of application. For graphical user interfaces, these are mainly the management of windows, menus, printing, and other generic services. A programmer need only provide the rest of the application, namely the parts which are unique to it. This is termed extending the framework.
One potential benefit of using an application framework becomes immediately clear when we look at the source code for a graphical user interface (GUI) program that was written completely from scratch: it can take pages of code merely to initialize and open a window with its menus. Application frameworks can dramatically reduce the effort to write GUI applications by eliminating such chores.
In contrast to subroutine libraries, an application framework allows much more than mere code reuse. It defines a structure for the framework's extensions, i.e. a blue-print which can be followed to get a complete application with the least possible effort.
One tool for defining a structure for other programs is called object-orientation: the framework defines object types, which the programmer can extend for his purposes. By extending a type both structure (the object's interface) as well as default behavior is inherited (code reuse).
An interesting property of an object-oriented application framework is that the framework can manipulate an object about which it only knows the interface (since it defined this interface in the first place), but whose implementation it doesn't know. In particular, the implementation may be created after the release of the interface, it may be created by someone else, and there even may be several different implementations in use concurrently.
In contrast to classical subroutine libraries, a framework's services are not only called by an application, but the framework also calls the application. A call from framework to application (i.e. extension) is called an "upcall". How this term is justified can be seen in the following diagram, where an arrow describes who calls whom:
Picture 1.2a Typical Control Flows in a Subroutine
Library and in an Application Framework
Sometimes this is called the "Hollywood Principle" of object-oriented programming, which says:
Don't call us, we'll call you.
In reality, the situation is more involved. An extension sometimes does call the framework, an extension can itself be extensible and act as a more specialized framework, and several extensions may work together "sideways" as well. These potentially confusing possibilities underline the importance of structure, which is afforded by the framework. For large software systems, a good system structure (sometimes called a system design or system architecture) becomes far more important than the issue of code reuse, for example. In practice, a bad system structure quickly limits extensibility and integration.
1.3 Compound Documents
Picture 1.3a Window Containing a Compound Document
There is a major difference between Oberon/F and most application frameworks on the market today. In contrast to these products, Oberon/F is not centered around the concept of "applications", but rather around "views":
In the traditional, application-centered interpretation of programming, an application is opened first. Then, one or more documents are opened, edited, and closed eventually. Finally, the application is terminated. At any time, an open document "belongs" to exactly one application.
If the same document should be used with several applications simultaneously, the user must switch back and forth between applications; and open, close, and possibly even convert the document upon any switch.
But consider the common case of a text document with an embedded graphics box. This is a document which should be editable both by a word processor and by a graphics editor. The more object types (tables, spreadsheets, pictures...) are supported, the clearer the need for a different computing paradigm becomes: The user is interested first and foremost in his documents, and these documents should contain whatever objects are useful for his work. The software behind these objects should be available whenever needed, and not stand in the user's way.
To support such a compound document approach is more difficult to realize than programs in the traditional style, because software components must cooperate rather than ignore each other. However, the benefits for the user are tremendous, because it gives him the freedom to choose and combine the tools that he really wants, rather than hoping that any one company can develop and incorporate all the industry's best tools into one super program.
In fact, compound documents are a strong argument in favour of application frameworks, because a framework can predefine a hierarchical embedding infrastructure for visible objects. Such objects are called views in Oberon/F. All views are defined as extensions of the type View which is exported by module Views. There is no need to write different code for objects living in a window (the classic case) and for objects living in other objects (embedded objects).
In the future, Oberon/F may also support emerging industry standards like Microsoft's OLE 2.0 and the Component Integration Lab's OpenDoc proposal for a compound document standard.
1.4 From Application Frameworks to Component Frameworks
One reason why compound document support has been so slow in coming, and now is being retrofitted into existing operating systems in astoundingly complex and convoluted ways, was the lack of an important operating system feature: shared libraries, also called dynamic link libraries. Dynamic link libraries (DLLs) are software components which are loaded only when they are needed, and which can be shared between different applications; rather than being linked into each and every application, wasting vast amounts of memory and disk space.
Object embedding facilities like Apple's Publish and Subscribe or Microsoft's OLE are so complicated and inefficient not least because they essentially try to use a complete and traditionally structured application program as a kind of DLL.
A compiled Oberon/L module can be thought of as a dynamic link library. Sadly though, many commercial implementations of DLLs are inefficient or have been designed with subroutine libraries in mind, rather than class libraries. Thus they sometimes lack support for object types, for global variables, or for version management. As a consequence, some Oberon/F implementations are forced to implement their own DLL mechanisms. In the future, technologies like IBM's SOM (System Object Model) may help solve these problems.
Why are DLLs important? Consider our earlier example of a text document which includes a graphics box. Now the user decides that he wants to insert a spreadsheet object as well, but he has no spreadsheet application/tool available. Thus he goes to the next store, buys a spreadsheet component, copies this component to his hard disk, and then is immediately able to insert a spreadsheet object into his document. At least that's the way it should be.
What does that mean? It means that the user has extended his software with a new component. Traditionally, this was the prerogative of a programmer: the programmer defined when a program was complete, and then linked together all components. From then on, the application was monolithic and frozen. With DLLs, the linking step is delayed until a module is actually needed, e.g. when a document containing a spreadsheet object is opened.
From a user's perspective, a shift occurred away from monolithic stand-alone programs toward software components which can be integrated with each other. From a programmer's perspective, a shift from application frameworks to component frameworks becomes necessary. Oberon/F is the first commercial Oberon component framework.
In Oberon/F, every "application" immediately supports compound documents, because an application really is a view type with some auxiliary commands, packed into a collection of modules. And views are Oberon/F's document components; every view can immediately be embedded in any document editor. Moreover, what we call a "document editor" here is just a powerful kind of view as well, called a container view. Each container view is able to embed any other view.
1.5 Garbage Collection
Extensible programs are never frozen, and thus there is no programmer who can know the whole software environment in which his particular piece of software will operate eventually. Software components can be tightly integrated, which means that several extensions may access the same data structure simultaneously, provided that this data structure is exported. Unfortunately, this also means that no programmer can know for sure whether this data structure is not used anymore, by someone else. This raises the question of when to deallocate a data structure, and by whom.
If memory is deallocated late, it wastes memory. Many programs contain errors which cause some data structures to remain in memory forever, although they are not referenced anymore (memory leaks).
On the other hand, if memory is deallocated too early, i.e. when it is still being referenced, it may be re-allocated by someone else (dangling pointers). This is even worse than memory leaks, because several variables then use the same memory location to store different values. This is an error which has the character of a time-bomb: its effect may become manifest (often as a violent crash) much later than the error actually occured, and at a seemingly unrelated place. This lack of locality makes such errors extremely hard to find.
This problem is so severe that it has spawned a whole industry providing tools to deal with it, with limited success so far.
The problem is hard for closed programs, i.e. programs whose source code is completely accessible to a programmer. But for extensible programs ("open systems"), these problems increase exponentially, since the cause of a crash may be any of the loaded extensions, whose internals are not known. The severity of the problem is further aggravated when compound documents are supported: a document is only as safe as the safety of the weakest object implementation used in this document.
Traditional solutions to such safety issues are not applicable here: usual hardware protection schemes prevent exactly the kind of tight integration which is necessary for compound documents, and which is essential for efficiency.
The root of the problem is that in an open world, no one knows the whole program (since this concept doesn't exist anymore), and thus no one knows when memory can be deallocated safely.
Oberon doesn't have this problem at all: it leaves memory reclamation to a trusted system service, instead of burdening the programmer with explicit deallocation. In essence, such a garbage collector tries to prove for every memory block that it is not being referenced anymore. If the proof succeeds, it gives the block free for future use.
Garbage collection is not directly a feature of object-orientation. However, if it is understood that the essence of object-oriented programming lies in extensibility combined with integration, it soon becomes clear that garbage collection is practically mandatory for systems which attempt to take full advantage of object-orientation.
1.6 The Architect's Tools of the Trade
The architecture of a software system is the most difficult thing to get right, and the most expensive thing to get wrong. While code reuse is often discussed and sought after in order to save time and money, it is less obvious that architecture reuse is a much more important issue. If the architecture is flawed, the implementation and maintenance efforts can be dramatically increased, and the life-time of the product can be considerably shortened because extensions become too difficult or unreliable.
In fact, the purpose of an object-oriented framework is mainly to define a suitable architecture for software components in a particular application domain, e.g. editors sporting graphical user interfaces.
A software architecture should be a structural backbone on which every programmer can rely. If a programming language allows to express architectural decisions explicitly, it becomes a most powerful computer-aided software engineering (CASE) tool, because a compiler for the language can verify the conformance of a program with its underlying architecture. The more it can check at compile-time, the better.
Programming languages differ in the degrees to which their constructs can express architectural decisions. In this respect, Oberon/L is one of the most powerful languages available today. Oberon/L provides several facilities to define the static structuring of a program; these facilities are essential tools for the software architect.
The largest structural unit is the compilation unit, a module. Modules define visibility boundaries (for information hiding). What is inside a module (i.e. constants, variables, types, procedures) cannot be seen (i.e. used, called, or modified) outside of this module, except if it is exported explicitly by the module. The exported items of a module are called its interface. A module may import the interfaces of other modules. Modules combine items which together form an abstraction, e.g. the abstraction of a file system. Such an abstraction usually entails not only one abstract data type or class, but a whole clique of related object types, as well as constants, global variables, and subroutines.
In Oberon, all software is composed of modules, whether it is an operating system, an application framework, an application, or a tool. What is considered the operating system is thus not so much a technical issue, but more a matter of convention. Calls between modules are just normal procedure calls, there are no such things as special kernel calls or similar special constructs.
In systems which are not written entirely in Oberon, platform-specific language extensions may be provided, with the sole purpose of interfacing to foreign software, e.g. to the host operating system. Such language extensions must be optimized for the given host system, thus they are not portable and are not considered part of the language proper. Their use is normally limited to a few modules of a program.
The availability of a module construct is a major advantage of Oberon compared to most current object-oriented programming languages.
A program can be described in terms of its module graph. This is a directed acyclic graph which shows who imports whom. Calls along an edge of such a graph are upcalls, while calls in the opposite direction are normal subroutine calls:
Picture 1.6a Example of a Module Graph
Modules combine items which are related, e.g. several object types which work together to perform some task. The interfaces between modules should be kept as "thin" as possible.
The second important static structuring facility of Oberon is its type system. Every variable has a type, which defines the set of values that the variable may contain, and the operations which are legal on this variable. Besides several basic types like characters, integers, or sets, Oberon provides mainly two kinds of structured types, namely arrays and records. Arrays are vectors of values with the same type, while records are tuples of values which may have different types. An array element is accessed via an index, while a record field is accessed via a name. Array elements and record fields may be records, arrays, or variables of any other of the basic types. This makes it possible to combine types in a recursive way. Pointer types and procedure types are further powerful type constructs.
In order to support object-oriented programming, Oberon/L provides record extension and type-bound procedures. Record extension allows to declare one record type (sometimes called a class) as an extension of another record type. The extending type is compatible to the extended type (base type), which means that wherever a variable of the base type can be used, a variable of the extension type may be used as well.
A pointer type declares the record type to which it is bound, this is the so-called static type. However, the static type of a variable only defines a minimal requirement for the contents of this variable. Any extension of the static type (which thus is compatible to it!) can be assigned to the variable as well. This is called the dynamic type of the pointer variable.
Using record extension and pointers, polymorphic data structures can be built. An example of a polymorphic data structure is the list of windows managed by a window system: the list may contain document windows as well as dialogs. Both may be declared as extensions of a basic window type.
A type-bound procedure (sometimes called a method) is a procedure that is bound to a pointer (or its record) type. This means that depending on the pointer variable's dynamic type, a different procedure may actually be called. As a result, a pointer variable - which can appropriately be called an object now - may behave differently, depending on its dynamic type. Let us suppose that a window pointer has a procedure Close bound to it. Then a dialog box (which is an extended window) may simply close the window when this procedure is called, while a document window may first ask whether its contents should be stored on disk before closing.
Strong typing, strong modularity, and the integration of object-oriented programming techniques into a proven type system are among the important advantages of the Oberon language, making it not only an implementer's tool, but also a fundamental tool for the software architect.
1.7 From White-Box Frameworks towards Black-Box Frameworks
In Oberon/L, a procedure may be bound to a record or pointer type. When a record type is extended, the extending type inherits all procedures bound to the base type. However, every inherited procedure may be overridden, i.e. the procedure's implementation may be redefined.
Overriding is a powerful feature, since it allows the replacement or modification of inherited (default) behavior. However, strict rules must be followed in order to use overriding correctly. Incorrect use leads to the so-called fragile base class problem, where the behavior of a base type cannot be modified anymore without breaking some of its extension types.
The general rule for a correct use of overriding is the following:
An overriding procedure must be compatible to the overridden procedure both syntactically and semantically.
Syntactic compatibility can be expressed in the language and checked by the compiler. Alas, semantic compatibility is more difficult to achieve. This is the cause of many design errors in class libraries.
Instead of using the term "overriding", it is much less misleading to use the term "extending". A procedure does not override or redefine another procedure, it much rather extends it, i.e. it does everything the base procedure does, plus something more. This immediately lets us conclude that the base procedure should be called in the extending procedure, with a super call. This guarantees that whatever the base procedure does, will also be done by the extending procedure, and in exactly the same way. Which is something we can expect of a semantically compatible procedure.
There is a special case where we don't want to call a base procedure in an extended procedure, namely when the base procedure provides a default behavior which can be re-implemented in a more efficient way. As an example, consider a file object which provides two procedures, one for writing a single byte, and another one for writing an array of bytes. Obviously, the second procedure can be implemented in terms of the first one. This is reasonable to do as a default behavior. A particular file implementation however, e.g. for disk files, may increase performance considerably by re-implementing the same functionality directly.
If a base procedure is designed as a default procedure, its behavior must be specified completely (no hidden behavior), and everything needed for a re-implementation must be exported, i.e. must be available to possible re-implementers.
A special case of a default procedure is the empty procedure. For efficiency, an empty procedure should not be called by an extending procedure via a super call. Empty procedures are called hook procedures.
An empty procedure can be useful, because it forms part of its object's interface. If this is the only function of an empty procedure, we call it an interface procedure. An interface procedure is a procedure definition without a corresponding implementation, because the implementation cannot be provided where the interface is defined. The implementation must be delivered by the extensions instead. A procedure which is not an interface procedure is called a concrete procedure.
A data type which provides one or more interface procedures is called an interface type. Interface types are only interfaces, they have missing or at least incomplete implementations, and thus no variable of such a type can ever be used. Interface procedures must be overridden in any concrete extension type, and such a concrete type (i.e. a direct extension of an interface type) must not perform a super call. In Oberon/F, there is a convention to call the HALT statement in the body of an interface procedure, to detect any incorrect use.
Only variables of concrete types may be allocated and used. It is the user's responsibility to only allocate variables of concrete types.
Extending concrete types is dangerous, because a concrete type has much more specialized semantics than an interface type. This means that extensions of concrete types are likely to have changed (i.e. incompatible) semantics, instead of extended semantics as would be proper. In particular, complicated calling sequences can occur (upcalls, downcalls, super calls), resulting in very complex interactions of the side effects caused by these procedures. There is no way to specify (and thus to fully understand) such side effect interactions, except to publish all source code.
Frameworks which can only be used given their whole source code are called white-box frameworks. Experience shows that white-box frameworks suffer from two fundamental problems:
The first one is complexity: since interface cannot be separated from implementation in a white-box framework, the whole framework's implementation must be studied and understood by a programmer. This becomes virtually impossible for large and complex frameworks.
The second problem with the lacking distinction between interface and implementation is over-specification: Every implementation detail must be considered as part of the framework's specification, and thus cannot be changed anymore: the fragile base class problem mentioned earlier.
Let us summarize the problem with the extension of concrete types ("code inheritance"): A concrete procedure may internally execute statements, such as assignments or procedure calls, which modify the program's state. The exact nature and sequence of such modifications determine the behavior of the whole program, not only of the module or object to which the procedure belongs. In the presence of overriding and super calls, there exists no notation which allows to specify such effects precisely enough, except the program's complete source code. A program component whose complete source code has been published may not be modified anymore, since this might break extensions.
Since this was felt to be too restrictive, Oberon/F was designed more as a black-box framework instead: In general, concrete types cannot not extended in Oberon/F. This is achieved simply by not exporting them. Yet, type exension of interface types is used extensively, since this doesn't pose the described problems.
Abandonning code inheritance does not mean abandonning code reuse: Instead of reuse by inheritance, Oberon/F uses reuse by composition, e.g. a document is composed of views.